var should = require('should'),
    sinon = require('sinon'),
    _ = require('lodash'),
    Promise = require('bluebird'),
    ObjectId = require('bson-objectid'),
    permissions = require('../../../../server/services/permissions'),
    common = require('../../../../server/lib/common'),
    apiUtils = require('../../../../server/api/v0.1/utils'),

    sandbox = sinon.sandbox.create();

describe('API Utils', function () {
    afterEach(function () {
        sandbox.restore();
    });

    it('exports', function () {
        // @TODO reduce the number of methods that are here!
        _.keys(apiUtils).should.eql([
            'globalDefaultOptions',
            'dataDefaultOptions',
            'browseDefaultOptions',
            'idDefaultOptions',
            'validate',
            'validateOptions',
            'detectPublicContext',
            'applyPublicPermissions',
            'handlePublicPermissions',
            'handlePermissions',
            'trimAndLowerCase',
            'prepareInclude',
            'prepareFields',
            'prepareFormats',
            'convertOptions',
            'checkObject'
        ]);
    });

    describe('Default Options', function () {
        it('should provide a set of default options', function () {
            apiUtils.globalDefaultOptions.should.eql(['context', 'include']);
            apiUtils.browseDefaultOptions.should.eql(['page', 'limit', 'fields', 'filter', 'order', 'debug']);
            apiUtils.dataDefaultOptions.should.eql(['data']);
            apiUtils.idDefaultOptions.should.eql(['id']);
        });
    });

    describe('validate', function () {
        it('should create options when passed no args', function (done) {
            apiUtils.validate()().then(function (options) {
                options.should.eql({});
                done();
            }).catch(done);
        });

        it('should pick data attrs when passed them', function (done) {
            apiUtils.validate('test', {attrs: ['id']})(
                {id: 'test', status: 'all', uuid: 'other-test'}
            ).then(function (options) {
                options.should.have.ownProperty('data');
                options.data.should.have.ownProperty('id');
                options.should.not.have.ownProperty('id');
                options.data.id.should.eql('test');

                options.data.should.not.have.ownProperty('status');
                options.should.not.have.ownProperty('status');

                options.should.not.have.ownProperty('uuid');
                done();
            }).catch(done);
        });

        it('should pick data attrs & leave options if passed', function (done) {
            apiUtils.validate('test', {attrs: ['id'], opts: ['status', 'uuid']})(
                {id: 'test', status: 'all', uuid: 'ffecea44-393c-4273-b784-e1928975ecfb'}
            ).then(function (options) {
                options.should.have.ownProperty('data');
                options.data.should.have.ownProperty('id');
                options.should.not.have.ownProperty('id');
                options.data.id.should.eql('test');

                options.data.should.not.have.ownProperty('status');
                options.should.have.ownProperty('status');
                options.status.should.eql('all');

                options.should.have.ownProperty('uuid');
                options.uuid.should.eql('ffecea44-393c-4273-b784-e1928975ecfb');
                done();
            }).catch(done);
        });

        it('should check data if an object is passed', function (done) {
            var object = {test: [{id: 1}]},
                checkObjectStub = sandbox.stub(apiUtils, 'checkObject').returns(Promise.resolve(object));

            apiUtils.validate('test')(object, {}).then(function (options) {
                checkObjectStub.calledOnce.should.be.true();
                checkObjectStub.calledWith(object, 'test').should.be.true();
                options.should.have.ownProperty('data');
                options.data.should.have.ownProperty('test');
                done();
            }).catch(done);
        });

        it('should handle options being undefined', function (done) {
            apiUtils.validate()(undefined).then(function (options) {
                options.should.eql({});
                done();
            }).catch(done);
        });

        it('should handle options being undefined when provided with object', function (done) {
            var object = {test: [{id: 1}]},
                checkObjectStub = sandbox.stub(apiUtils, 'checkObject').returns(Promise.resolve(object));

            apiUtils.validate('test')(object, undefined).then(function (options) {
                checkObjectStub.calledOnce.should.be.true();
                checkObjectStub.calledWith(object, 'test').should.be.true();
                options.should.have.ownProperty('data');
                options.data.should.have.ownProperty('test');
                done();
            }).catch(done);
        });

        it('should remove unknown options', function (done) {
            apiUtils.validate('test')({magic: 'stuff', rubbish: 'stuff'}).then(function (options) {
                options.should.not.have.ownProperty('data');
                options.should.not.have.ownProperty('rubbish');
                options.should.not.have.ownProperty('magic');
                done();
            }).catch(done);
        });

        it('should always allow context & include options', function (done) {
            apiUtils.validate('test')({context: 'stuff', include: 'stuff'}).then(function (options) {
                options.should.not.have.ownProperty('data');
                options.should.have.ownProperty('context');
                options.context.should.eql('stuff');
                options.should.have.ownProperty('include');
                options.include.should.eql('stuff');
                done();
            }).catch(done);
        });

        it('should allow page & limit options when browseDefaultOptions passed', function (done) {
            apiUtils.validate('test', {opts: apiUtils.browseDefaultOptions})(
                {context: 'stuff', include: 'stuff', page: 1, limit: 5}
            ).then(function (options) {
                options.should.not.have.ownProperty('data');
                options.should.have.ownProperty('context');
                options.context.should.eql('stuff');
                options.should.have.ownProperty('include');
                options.include.should.eql('stuff');
                options.should.have.ownProperty('page');
                options.page.should.eql(1);
                options.should.have.ownProperty('limit');
                options.limit.should.eql(5);
                done();
            }).catch(done);
        });

        it('should allow idDefaultOptions when passed', function (done) {
            var id = ObjectId.generate();

            apiUtils.validate('test', {opts: apiUtils.idDefaultOptions})(
                {id: id, context: 'stuff'}
            ).then(function (options) {
                options.should.not.have.ownProperty('data');
                options.should.not.have.ownProperty('include');
                options.should.not.have.ownProperty('page');
                options.should.not.have.ownProperty('limit');

                options.should.have.ownProperty('context');
                options.context.should.eql('stuff');
                options.should.have.ownProperty('id');
                options.id.should.eql(id);

                done();
            }).catch(done);
        });

        it('should reject if limit is invalid', function (done) {
            apiUtils.validate('test', {opts: apiUtils.browseDefaultOptions})(
                {limit: 'none'}
            ).then(function () {
                done(new Error('Should have thrown a validation error'));
            }).catch(function (err) {
                err.should.have.property('errorType', 'ValidationError');
                done();
            });
        });

        it('should reject if from is invalid', function (done) {
            apiUtils.validate('test', {opts: ['from']})(
                {from: true}
            ).then(function () {
                done(new Error('Should have thrown a validation error'));
            }).catch(function (err) {
                err.should.have.property('errorType', 'ValidationError');
                done();
            });
        });
    });

    describe('validateOptions', function () {
        var valid, invalid;

        function check(key, valid, invalid) {
            _.each(valid, function (value) {
                var options = {};
                options[key] = value;
                apiUtils.validateOptions(options).should.eql([]);
            });

            _.each(invalid, function (value) {
                var options = {}, errors;
                options[key] = value;

                errors = apiUtils.validateOptions(options);
                errors.should.be.an.Array().and.have.lengthOf(1);
                errors[0].errorType.should.eql('ValidationError');
            });
        }

        it('can validate `id`', function () {
            valid = [ObjectId.generate(), '1', 1];
            invalid = ['test', 'de305d54', 300, '304'];

            check('id', valid, invalid);
        });

        it('can validate `uuid`', function () {
            valid = ['de305d54-75b4-431b-adb2-eb6b9e546014'];
            invalid = ['de305d54-75b4-431b-adb2'];

            check('uuid', valid, invalid);
        });

        it('can validate `page`', function () {
            valid = [1, '1', 304, '304'];
            invalid = ['me', 'test', 'de305d54', -1, '-1'];

            check('page', valid, invalid);
        });

        it('can validate `limit`', function () {
            valid = [1, '1', 304, '304', 'all'];
            invalid = ['me', 'test', 'de305d54', -1, '-1'];

            check('limit', valid, invalid);
        });

        it('can validate `slug` or `status` or `author` etc as a-z, 0-9, - and _', function () {
            valid = ['hello-world', 'hello', '1-2-3', 1, '-1', -1, 'hello_world'];
            invalid = ['hello~world', '!things', '?other-things', 'thing"', '`ticks`'];

            check('slug', valid, invalid);
            check('status', valid, invalid);
            check('author', valid, invalid);
        });

        it('gives no errors for `context`, `include` and `data`', function () {
            apiUtils.validateOptions({
                context: {user: 1},
                include: '"super,@random!,string?and',
                data: {object: 'thing'}
            }).should.eql([]);
        });
    });

    describe('prepareInclude', function () {
        it('should handle empty items', function () {
            apiUtils.prepareInclude('', []).should.eql([]);
        });

        it('should be empty if there are no allowed includes', function () {
            apiUtils.prepareInclude('a,b,c', []).should.eql([]);
        });

        it('should return correct includes', function () {
            apiUtils.prepareInclude('a,b,c', ['a']).should.eql(['a']);
            apiUtils.prepareInclude('a,b,c', ['a', 'c']).should.eql(['a', 'c']);
            apiUtils.prepareInclude('a,b,c', ['a', 'd']).should.eql(['a']);
            apiUtils.prepareInclude('a,b,c', ['d']).should.eql([]);
        });
    });

    describe('convertOptions', function () {
        it('should not call prepareInclude if there is no include option', function () {
            var prepareIncludeStub = sandbox.stub(apiUtils, 'prepareInclude');
            apiUtils.convertOptions(['a', 'b', 'c'])({}).should.eql({});
            prepareIncludeStub.called.should.be.false();
        });

        it('should pass options.include to prepareInclude if provided', function () {
            var expectedResult = ['a', 'b'],
                prepareIncludeStub = sandbox.stub(apiUtils, 'prepareInclude').returns(expectedResult),
                allowed = ['a', 'b', 'c'],
                options = {include: 'a,b'},
                actualResult;

            actualResult = apiUtils.convertOptions(allowed)(_.clone(options));

            prepareIncludeStub.calledOnce.should.be.true();
            prepareIncludeStub.calledWith(options.include, allowed).should.be.true();

            actualResult.should.have.hasOwnProperty('withRelated');
            actualResult.withRelated.should.be.an.Array();
            actualResult.withRelated.should.eql(expectedResult);
        });
    });

    describe('checkObject', function () {
        it('throws an error if the object is empty', function (done) {
            apiUtils.checkObject({}, 'test').then(function () {
                done('This should have thrown an error');
            }).catch(function (error) {
                should.exist(error);
                error.errorType.should.eql('BadRequestError');
                done();
            });
        });

        it('throws an error if the object key is empty', function (done) {
            apiUtils.checkObject({test: []}, 'test').then(function () {
                done('This should have thrown an error');
            }).catch(function (error) {
                should.exist(error);
                error.errorType.should.eql('BadRequestError');
                done();
            });
        });

        it('throws an error if the object key is array with empty object', function (done) {
            apiUtils.checkObject({test: [{}]}, 'test').then(function () {
                done('This should have thrown an error');
            }).catch(function (error) {
                should.exist(error);
                error.errorType.should.eql('BadRequestError');
                done();
            });
        });

        it('passed through a simple, correct object', function (done) {
            var object = {test: [{id: 1}]};
            apiUtils.checkObject(_.cloneDeep(object), 'test').then(function (data) {
                should.exist(data);
                data.should.have.ownProperty('test');
                object.should.eql(data);
                done();
            }).catch(done);
        });

        it('passed through a simple, correct object', function (done) {
            var object = {test: [{id: 1}]};
            apiUtils.checkObject(_.cloneDeep(object), 'test').then(function (data) {
                should.exist(data);
                data.should.have.ownProperty('test');
                object.should.eql(data);
                done();
            }).catch(done);
        });

        it('[DEPRECATED] should do author_id to author conversion for posts', function (done) {
            var object = {posts: [{id: 1, author: 4}]};
            apiUtils.checkObject(_.cloneDeep(object), 'posts').then(function (data) {
                should.exist(data);
                data.should.have.ownProperty('posts');
                data.should.not.eql(object);
                data.posts.should.be.an.Array();
                data.posts[0].should.have.ownProperty('author_id');
                data.posts[0].should.not.have.ownProperty('author');
                done();
            }).catch(done);
        });

        it('[DEPRECATED] should not do author_id to author conversion for posts if not needed', function (done) {
            var object = {posts: [{id: 1, author_id: 4}]};
            apiUtils.checkObject(_.cloneDeep(object), 'posts').then(function (data) {
                should.exist(data);
                data.should.have.ownProperty('posts');
                data.should.eql(object);
                data.posts.should.be.an.Array();
                data.posts[0].should.have.ownProperty('author_id');
                data.posts[0].should.not.have.ownProperty('author');
                done();
            }).catch(done);
        });

        it('should throw error if invalid editId if provided', function (done) {
            var object = {test: [{id: 1}]};
            apiUtils.checkObject(_.cloneDeep(object), 'test', 3).then(function () {
                done('This should have thrown an error');
            }).catch(function (error) {
                should.exist(error);
                error.errorType.should.eql('BadRequestError');
                done();
            });
        });

        it('should ignore undefined editId', function (done) {
            var object = {test: [{id: 1}]};
            apiUtils.checkObject(_.cloneDeep(object), 'test', undefined).then(function (data) {
                should.exist(data);
                data.should.eql(object);
                done();
            }).catch(done);
        });

        it('should ignore editId if object has no id', function (done) {
            var object = {test: [{uuid: 1}]};
            apiUtils.checkObject(_.cloneDeep(object), 'test', 3).then(function (data) {
                should.exist(data);
                data.should.eql(object);
                done();
            }).catch(done);
        });

        it('will delete null values from object', function (done) {
            var object = {test: [{id: 1, key: null}]};

            apiUtils.checkObject(_.cloneDeep(object), 'test').then(function (data) {
                should.not.exist(data.test[0].key);
                should.exist(data.test[0].id);
                done();
            }).catch(done);
        });

        it('will not break if the expected object is a string', function (done) {
            var object = {test: ['something']};

            apiUtils.checkObject(_.cloneDeep(object), 'test').then(function (data) {
                data.test[0].should.eql('something');
                done();
            }).catch(done);
        });

        describe('post.tags structure', function () {
            it('post.tags is empty', function (done) {
                var object = {
                    posts: [{
                        id: 1,
                        tags: []
                    }]
                };

                apiUtils.checkObject(_.cloneDeep(object), 'posts')
                    .then(function (object) {
                        object.posts[0].tags.length.should.eql(0);
                        done();
                    })
                    .catch(done);
            });

            it('post.tags contains `parent`', function (done) {
                var object = {
                    posts: [{
                        id: 1,
                        tags: [{id: 'objectid', parent: null}]
                    }]
                };

                apiUtils.checkObject(_.cloneDeep(object), 'posts')
                    .then(function (object) {
                        object.posts[0].tags[0].hasOwnProperty('parent').should.be.false();
                        object.posts[0].tags[0].hasOwnProperty('parent_id').should.be.true();
                        done();
                    })
                    .catch(done);
            });

            it('post.tags contains `parent_id`', function (done) {
                var object = {
                    posts: [{
                        id: 1,
                        tags: [{id: 'objectid', parent_id: null}]
                    }]
                };

                apiUtils.checkObject(_.cloneDeep(object), 'posts')
                    .then(function (object) {
                        object.posts[0].tags[0].hasOwnProperty('parent').should.be.false();
                        object.posts[0].tags[0].hasOwnProperty('parent_id').should.be.true();
                        done();
                    })
                    .catch(done);
            });

            it('post.tags contains no parent', function (done) {
                var object = {
                    posts: [{
                        id: 1,
                        tags: [{id: 'objectid'}]
                    }]
                };

                apiUtils.checkObject(_.cloneDeep(object), 'posts')
                    .then(function (object) {
                        object.posts[0].tags[0].hasOwnProperty('parent').should.be.false();
                        object.posts[0].tags[0].hasOwnProperty('parent_id').should.be.false();
                        done();
                    })
                    .catch(done);
            });
        });

        describe('post.authors structure', function () {
            it('post.authors is not present', function (done) {
                var object = {posts: [{id: 1}]};

                apiUtils.checkObject(_.cloneDeep(object), 'posts')
                    .then(function () {
                        done();
                    })
                    .catch(done);
            });

            it('post.authors is no array', function (done) {
                var object = {posts: [{id: 1, authors: null}]};

                apiUtils.checkObject(_.cloneDeep(object), 'posts')
                    .then(function () {
                        "Test should fail".should.eql(false);
                    })
                    .catch(function (err) {
                        (err instanceof common.errors.BadRequestError).should.be.true;
                        err.message.should.eql('No valid object structure provided for: posts[*].authors');
                        done();
                    });
            });

            it('post.authors is empty', function (done) {
                var object = {posts: [{id: 1, authors: []}]};

                apiUtils.checkObject(_.cloneDeep(object), 'posts')
                    .then(function () {
                        done();
                    })
                    .catch(done);
            });

            it('post.authors contains id property', function (done) {
                var object = {
                    posts: [{
                        id: 1,
                        authors: [{id: 'objectid', name: 'Kate'}, {id: 'objectid', name: 'Steffen'}]
                    }]
                };

                apiUtils.checkObject(_.cloneDeep(object), 'posts')
                    .then(function () {
                        done();
                    })
                    .catch(done);
            });

            it('post.authors does not contain id property', function (done) {
                var object = {posts: [{id: 1, authors: [{id: 'objectid', name: 'Kate'}, {name: 'Steffen'}]}]};

                apiUtils.checkObject(_.cloneDeep(object), 'posts')
                    .then(function () {
                        "Test should fail".should.eql(false);
                    })
                    .catch(function (err) {
                        (err instanceof common.errors.BadRequestError).should.be.true;
                        err.message.should.eql('No valid object structure provided for: posts[*].authors');
                        done();
                    });
            });

            it('post.authors contains nested relations', function (done) {
                var object = {
                    posts: [{
                        id: 1,
                        authors: [{id: 'objectid', name: 'Kate', roles: [{id: 'something'}], permissions: []}]
                    }]
                };

                apiUtils.checkObject(_.cloneDeep(object), 'posts')
                    .then(function (object) {
                        should.not.exist(object.posts[0].authors[0].roles);
                        should.not.exist(object.posts[0].authors[0].permissions);
                        done();
                    })
                    .catch(done);
            });
        });
    });

    describe('isPublicContext', function () {
        it('should call out to permissions', function () {
            var permsStub = sandbox.stub(permissions, 'parseContext').returns({public: true});
            apiUtils.detectPublicContext({context: 'test'}).should.be.true();
            permsStub.called.should.be.true();
            permsStub.calledWith('test').should.be.true();
        });
    });

    describe('applyPublicPermissions', function () {
        it('should call out to permissions', function () {
            var permsStub = sandbox.stub(permissions, 'applyPublicRules');
            apiUtils.applyPublicPermissions('test', {});
            permsStub.called.should.be.true();
            permsStub.calledWith('test', {}).should.be.true();
        });
    });

    describe('handlePublicPermissions', function () {
        it('should return empty options if passed empty options', function (done) {
            apiUtils.handlePublicPermissions('tests', 'test')({}).then(function (options) {
                options.should.eql({context: {app: null, external: false, internal: false, public: true, user: null}});
                done();
            }).catch(done);
        });

        it('should treat no context as public', function (done) {
            var aPPStub = sandbox.stub(apiUtils, 'applyPublicPermissions').returns(Promise.resolve({}));
            apiUtils.handlePublicPermissions('tests', 'test')({}).then(function (options) {
                aPPStub.calledOnce.should.eql(true);
                options.should.eql({context: {app: null, external: false, internal: false, public: true, user: null}});
                done();
            }).catch(done);
        });

        it('should treat user context as NOT public', function (done) {
            var cTMethodStub = {
                    test: {
                        test: sandbox.stub().returns(Promise.resolve())
                    }
                },
                cTStub = sandbox.stub(permissions, 'canThis').returns(cTMethodStub);

            apiUtils.handlePublicPermissions('tests', 'test')({context: {user: 1}}).then(function (options) {
                cTStub.calledOnce.should.eql(true);
                cTMethodStub.test.test.calledOnce.should.eql(true);
                options.should.eql({context: {app: null, external: false, internal: false, public: false, user: 1}});
                done();
            }).catch(done);
        });

        it('should throw a permissions error if permission is not granted', function (done) {
            var cTMethodStub = {
                    test: {
                        test: sandbox.stub().returns(Promise.reject(new common.errors.NoPermissionError()))
                    }
                },
                cTStub = sandbox.stub(permissions, 'canThis').returns(cTMethodStub);

            apiUtils.handlePublicPermissions('tests', 'test')({context: {user: 1}}).then(function () {
                done(new Error('should throw error when no permissions'));
            }).catch(function (err) {
                cTStub.calledOnce.should.eql(true);
                cTMethodStub.test.test.calledOnce.should.eql(true);
                err.errorType.should.eql('NoPermissionError');
                done();
            });
        });
    });

    describe('handlePermissions', function () {
        it('should require a docName', function () {
            apiUtils.handlePermissions.should.throwError();
        });

        it('should return a function', function () {
            apiUtils.handlePermissions('test').should.be.a.Function();
        });

        it('should handle an unknown rejection', function (done) {
            var testStub = sandbox.stub().returns(new Promise.reject(new Error('not found'))),
                permsStub = sandbox.stub(permissions, 'canThis').callsFake(function () {
                    return {
                        testing: {
                            test: testStub
                        }
                    };
                }),
                permsFunc = apiUtils.handlePermissions('tests', 'testing');

            permsFunc({})
                .then(function () {
                    done(new Error('Should have thrown an error'));
                })
                .catch(function (err) {
                    permsStub.callCount.should.eql(1);
                    testStub.callCount.should.eql(1);
                    err.errorType.should.eql('InternalServerError');

                    done();
                });
        });

        it('should handle a NoPermissions rejection', function (done) {
            var testStub = sandbox.stub().returns(Promise.reject(new common.errors.NoPermissionError())),
                permsStub = sandbox.stub(permissions, 'canThis').callsFake(function () {
                    return {
                        testing: {
                            test: testStub
                        }
                    };
                }),
                permsFunc = apiUtils.handlePermissions('tests', 'testing');

            permsFunc({})
                .then(function () {
                    done(new Error('Should have thrown an error'));
                })
                .catch(function (err) {
                    permsStub.callCount.should.eql(1);
                    testStub.callCount.should.eql(1);

                    err.errorType.should.eql('NoPermissionError');
                    err.message.should.match(/testing/);
                    err.message.should.match(/tests/);
                    done();
                });
        });

        it('should handle success', function (done) {
            var testStub = sandbox.stub().returns(new Promise.resolve()),
                permsStub = sandbox.stub(permissions, 'canThis').callsFake(function () {
                    return {
                        testing: {
                            test: testStub
                        }
                    };
                }),
                permsFunc = apiUtils.handlePermissions('tests', 'testing'),
                testObj = {foo: 'bar', id: 5};

            permsFunc(testObj)
                .then(function (res) {
                    permsStub.callCount.should.eql(1);
                    testStub.callCount.should.eql(1);
                    testStub.firstCall.args.length.should.eql(2);
                    testStub.firstCall.args[0].should.eql(5);
                    testStub.firstCall.args[1].should.eql({});

                    res.should.eql(testObj);

                    done();
                })
                .catch(done);
        });

        it('should ignore unsafe attrs if none are provided', function (done) {
            var testStub = sandbox.stub().returns(new Promise.resolve()),
                permsStub = sandbox.stub(permissions, 'canThis').callsFake(function () {
                    return {
                        testing: {
                            test: testStub
                        }
                    };
                }),
                permsFunc = apiUtils.handlePermissions('tests', 'testing', ['foo']),
                testObj = {data: {tests: [{}]}, id: 5};

            permsFunc(testObj)
                .then(function (res) {
                    permsStub.callCount.should.eql(1);
                    testStub.callCount.should.eql(1);
                    testStub.firstCall.args.length.should.eql(2);
                    testStub.firstCall.args[0].should.eql(5);
                    testStub.firstCall.args[1].should.eql({});

                    res.should.eql(testObj);

                    done();
                })
                .catch(done);
        });

        it('should ignore unsafe attrs if they are provided but not present', function (done) {
            var testStub = sandbox.stub().returns(new Promise.resolve()),
                permsStub = sandbox.stub(permissions, 'canThis').callsFake(function () {
                    return {
                        testing: {
                            test: testStub
                        }
                    };
                }),
                permsFunc = apiUtils.handlePermissions('tests', 'testing', ['foo']),
                testObj = {foo: 'bar', id: 5};

            permsFunc(testObj)
                .then(function (res) {
                    permsStub.callCount.should.eql(1);
                    testStub.callCount.should.eql(1);
                    testStub.firstCall.args.length.should.eql(2);
                    testStub.firstCall.args[0].should.eql(5);
                    testStub.firstCall.args[1].should.eql({});

                    res.should.eql(testObj);

                    done();
                })
                .catch(done);
        });

        it('should pass through unsafe attrs if they DO exist', function (done) {
            var testStub = sandbox.stub().returns(new Promise.resolve()),
                permsStub = sandbox.stub(permissions, 'canThis').callsFake(function () {
                    return {
                        testing: {
                            test: testStub
                        }
                    };
                }),
                permsFunc = apiUtils.handlePermissions('tests', 'testing', ['foo']),
                testObj = {data: {tests: [{foo: 'bar'}]}, id: 5};

            permsFunc(testObj)
                .then(function (res) {
                    permsStub.callCount.should.eql(1);
                    testStub.callCount.should.eql(1);
                    testStub.firstCall.args.length.should.eql(2);
                    testStub.firstCall.args[0].should.eql(5);
                    testStub.firstCall.args[1].should.eql({foo: 'bar'});

                    res.should.eql(testObj);

                    done();
                })
                .catch(done);
        });

        it('should strip excludedAttrs from data if permissions function returns them', function () {
            var testStub = sandbox.stub().resolves({excludedAttrs: ['foo']}),
                permsStub = sandbox.stub(permissions, 'canThis').returns({
                    testing: {
                        test: testStub
                    }
                }),
                permsFunc = apiUtils.handlePermissions('tests', 'testing'),
                testObj = {data: {tests: [{id: 5, name: 'testing', foo: 'bar'}]}, id: 5};

            return permsFunc(testObj).then(function (res) {
                permsStub.calledOnce.should.be.true();
                testStub.calledOnce.should.be.true();
                testStub.calledWithExactly(5, {}).should.be.true();

                should(res).deepEqual({
                    data: {
                        tests: [{id: 5, name: 'testing'}]
                    },
                    id: 5
                });
            });
        });
    });
});
